# -*- encoding: utf-8 -*-

"""
------------------------------------------
@File       : 61.guess_number.py
@Author     : maixiaochai
@Email      : maixiaochai@outlook.com
@CreatedOn  : 2021/7/27 15:03
------------------------------------------
     C/S系统，猜数字游戏，服务器来猜
     多线程版 改造为 协程版的过程
"""
import random
import math
import contextlib
import socket
from threading import Thread

WARMER = 'Warmer'
COLDER = 'Colder'
UNSURE = 'Unsure'
CORRECT = 'Correct'


class EOFError(Exception):
    pass


class ConnectionBase:
    def __init__(self, connection):
        self.connection = connection
        self.file = connection.makefile('rb')

    def send(self, command):
        line = command + '\n'
        data = line.encode()
        self.connection.send(data)

    def receive(self):
        line = self.file.readline()
        if not line:
            raise EOFError('Connection closed')

        return line[:-1].decode()


class UnknownCommandError(Exception):
    pass


class Session(ConnectionBase):
    """
        服务端
    """

    def __init__(self, *args):
        super().__init__(*args)
        self.secret = None
        self._clear_state(None, None)

    def _clear_state(self, lower, upper):
        self.lower = lower
        self.upper = upper
        self.secret = None
        self.guesses = []

    def set_params(self, parts):
        assert len(parts) == 3
        lower = int(parts[1])
        upper = int(parts[2])
        self._clear_state(lower, upper)

    def next_guess(self):
        # 如果之前已经猜出来了，就直接发送猜中的数据
        if self.secret is not None:
            return self.secret

        # 如果之前没猜出来，就猜一个之前没猜过的数字
        while True:
            guess = random.randint(self.lower, self.upper)
            if guess not in self.guesses:
                return guess

    def send_number(self):
        """
            发送猜测的数字
        """
        guess = self.next_guess()
        self.guesses.append(guess)
        self.send(format(guess))

    def receive_report(self, parts):
        """
            接收猜测的反馈，大了？小了？猜对了？
        """
        assert len(parts) == 2
        decision = parts[1]

        last = self.guesses[-1]
        if decision == CORRECT:
            self.secret = last

        print(f'Server: {last} is {decision}')

    def loop(self):
        """
            服务器主要方法，循环地解析客户端所传来的命令，并根据具体内容把这条命令派发给相关的方法去处理
            指令说明：
                PARAMS: 高速服务器下边界 (lower)和上边界 (upper)
                NUMBER: 让服务器进行一次猜测
                REPORT: 客户端在收到服务端猜数接结果后发来的报告
        """
        while command := self.receive():
            parts = command.split(' ')

            if parts[0] == 'PARAMS':
                self.set_params(parts)

            elif parts[0] == 'NUMBER':
                self.send_number()

            elif parts[0] == 'REPORT':
                self.receive_report(parts)

            else:
                raise UnknownCommandError(command)


class Client(ConnectionBase):
    """
        客户端
        同样需要保存会话时所用到相关的状态
    """

    def __init__(self, *args):
        super().__init__(*args)
        self.secret = None
        self.last_distance = None
        self._clear_state()

    def _clear_state(self):
        self.secret = None
        self.last_distance = None

    @contextlib.contextmanager
    def session(self, lower, upper, secret):
        print(f"Guess a number between {lower} and {upper}! "
              f"Shhhhh, it's {secret}.")

        self.secret = secret
        self.send(f"PARAMS {lower} {upper}")

        try:
            yield
        finally:
            self._clear_state()
            self.send('PARAMS 0 -1')

    def request_numbers(self, count):
        """
            向服务器发送 NUMBER 命令，让其进行一次猜数，
            如果没猜中，则要求服务器继续猜，直到猜对或者猜测次数超过count为止
        """
        for _ in range(count):
            self.send('NUMBER')
            data = self.receive()
            yield int(data)

            if self.last_distance == 0:
                return

    def report_outcome(self, number):
        """
            向服务器发送猜测的结果
        """
        new_distance = math.fabs(number - self.secret)
        decision = UNSURE

        if new_distance == 0:
            pass

        if new_distance == 0:
            decision = CORRECT

        elif self.last_distance is None:
            pass

        elif new_distance < self.last_distance:
            decision = WARMER

        elif new_distance > self.last_distance:
            decision = COLDER

        self.last_distance = new_distance

        self.send(f"REPORT {decision}")

        return decision


def handle_connection(connection):
    with connection:
        session = Session(connection)
        try:
            session.loop()
        except EOFError:
            pass


def run_server(address):
    with socket.socket() as listener:
        listener.bind(address)
        listener.listen()

        # while 循环，一直监听，只要有新连接就开启一个新线程
        while True:
            connection, _ = listener.accept()
            thread = Thread(
                target=handle_connection,
                args=(connection,),
                daemon=True
            )

            thread.start()


def run_client(address):
    """
        启动客户端，连玩两局

        这段代码特意使用了Python的很多特性，如 for循环、with语句、生成器、列表推导式等等
        这样写是想让我们稍后清楚地看到，把这种代码迁移到协程实现方案上面的工作量到底大不大
    """
    with socket.create_connection(address) as connection:
        client = Client(connection)

        with client.session(1, 5, 3):
            results = [(x, client.report_outcome(x))
                       for x in client.request_numbers(5)]

        with client.session(10, 15, 12):
            for number in client.request_numbers(5):
                outcome = client.report_outcome(number)
                results.append((number, outcome))

    return results


def main():
    address = ('127.0.0.1', 1234)
    server_thread = Thread(
        target=run_server, args=(address,), daemon=True)
    server_thread.start()

    results = run_client(address)
    for number, outcome in results:
        print(f'Client: {number} is {outcome}')


if __name__ == '__main__':
    main()
